/*
 * Copyright (c) 2008 Kevin Wetzels
 * 
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
package be.roam.jindy.ui.pagination;

import static java.lang.Math.max;
import static java.lang.Math.min;

import java.io.Serializable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * This pagination layout incorporates some standard pagination methods.
 * <p>
 * It will create a list of pagination items that layout the exact pagination.
 * For example, to create a pagination like
 * <code>PREVIOUS - 1 - ... - 6 - 7 - 8 - ... - 25 - NEXT</code>, you'd create a
 * pagination layout that displays the previous and next links, the first and
 * last pages and displays 1 page before and after the current page.
 * </p>
 * <p>
 * After that, invoke {@link #getItems()} to get the list of
 * {@link PaginationItem}, which will look like this (the number is the number
 * of the page it represents, true or false indicates whether the item is a flow
 * breaker or not):
 * </p>
 * <p>
 * <code>6,false - 1,false - -1,true - 6,false - 7,false - 8,false - -1,true - 25,false - 8,false</code>
 * </p>
 * <p>
 * The tests document the behavior of this class, but here's an example:
 * </p>
 * 
 * <code>
 * <pre> 
 * int currentPageNr = 5;
 * int nrItemsPerPage = 20;
 * int totalNrItems = 235;
 * int nrItemsBeforeAndAfterCurrentPage = 2;
 * boolean showPreviousAndNext = true;
 * boolean showFirstAndLast = true;
 * boolean breakFlow = true;
 * 
 * Pagination pagination = new Pagination(currentPageNr, nrItemsPerPage, totalNrItems);
 * PaginationLayout layout = new PaginationLayout(pagination, showPreviousAndNext, showFirstAndLast, nrItemsBeforeAndAfterCurrentPage, breakFlow);
 * String html = &quot;&quot;;
 * for (PaginationItem item : layout) {
 *     if (item.isCurrent() || item.isFlowBreaker()) {
 *         html += &quot;&lt;span&gt;&quot;;
 *     } else {
 *         html += &quot;&lt;a href='http://link.to/page/&quot; + item.getNr() + &quot;'&gt;&quot;;
 *     }
 *     
 *     if (item.isPreviousPageItem()) {
 *         html += &quot;&amp;lt;&amp;lt; Previous&quot;;
 *     } else if (item.isNextPageItem()) {
 *         html += &quot;Next &amp;gt;&amp;gt;&quot;;
 *     } else if (item.isFlowBreaker()) {
 *         html += &quot;...&quot;;
 *     } else {
 *         html += &quot;&quot; + item.getNr(); 
 *     }     
 *     
 *     if (item.isCurrent() || item.isFlowBreaker()) {
 *         html += &quot;&lt;/span&gt;&quot;;
 *     } else {
 *         html += &quot;&lt;/a&gt;&quot;;
 *     }
 *     html += &quot;\n&quot;;
 * }
 * System.out.println(html); 
 * </pre>
 * </code>
 * <p>
 * The resulting output would look like this:
 * </p>
 * 
 * <code>
 * <pre> 
 * &lt;a href='http://link.to/page/4'&gt;&amp;lt;&amp;lt; Previous&lt;/a&gt;
 * &lt;a href='http://link.to/page/1'&gt;1&lt;/a&gt;
 * &lt;span&gt;...&lt;/span&gt;
 * &lt;a href='http://link.to/page/3'&gt;3&lt;/a&gt;
 * &lt;a href='http://link.to/page/4'&gt;4&lt;/a&gt;
 * &lt;span&gt;5&lt;/span&gt;
 * &lt;a href='http://link.to/page/6'&gt;6&lt;/a&gt;
 * &lt;a href='http://link.to/page/7'&gt;7&lt;/a&gt;
 * &lt;span&gt;...&lt;/span&gt;
 * &lt;a href='http://link.to/page/12'&gt;12&lt;/a&gt;
 * &lt;a href='http://link.to/page/6'&gt;Next &amp;gt;&amp;gt;&lt;/a&gt;  
 * </pre>
 * </code>
 * 
 * @author Kevin Wetzels
 */
public class PaginationLayout implements Iterable<PaginationItem>, Serializable {

	private static final long serialVersionUID = -9144298634066679026L;

	private Pagination pagination;

	private boolean showPreviousAndNext;

	private boolean showFirstAndLast;

	private int nrItemsAroundCurrentPage;

	private boolean breakFlow;

	private LinkedList<PaginationItem> items;

	public PaginationLayout(Pagination pagination) {
		this(pagination, true, true, 2, true);
	}

	public PaginationLayout(Pagination pagination, boolean showPreviousAndNext, boolean showFirstAndLast, int nrItemsAroundCurrentPage) {
		this(pagination, showPreviousAndNext, showFirstAndLast, nrItemsAroundCurrentPage, true);
	}

	/**
	 * Constructor.
	 * 
	 * @param pagination
	 *            pagination object
	 * @param showPreviousAndNext
	 *            add previous and next pagination items to the front and back
	 *            of the list?
	 * @param showFirstAndLast
	 *            add pagination items for the first and last pages to the front
	 *            (after previous) and back (before next) of the list?
	 * @param nrItemsAroundCurrentPage
	 *            the number of items to add to the list before and after the
	 *            current page
	 * @param breakFlow
	 *            add separators between the current page (and pages before and
	 *            after) and first and last and/or previous and next
	 */
	public PaginationLayout(Pagination pagination, boolean showPreviousAndNext, boolean showFirstAndLast, int nrItemsAroundCurrentPage, boolean breakFlow) {
		this.pagination = pagination;
		this.showPreviousAndNext = showPreviousAndNext;
		this.showFirstAndLast = showFirstAndLast;
		this.nrItemsAroundCurrentPage = nrItemsAroundCurrentPage;
		this.breakFlow = breakFlow;
	}

	/**
	 * Checks if the pagination actually consist of a single page, in which case
	 * retrieving the items would probably be useless.
	 * 
	 * @return <code>true</code> when the pagination consists of a single page
	 */
	public boolean isMadeOfSinglePage() {
		return pagination.isSinglePage();
	}

	/**
	 * Returns the inverse of {@link #isMadeOfSinglePage()}.
	 * 
	 * @return the inverse of {@link #isMadeOfSinglePage()}
	 */
	public boolean isMadeOfMultiplesPages() {
		return !isMadeOfSinglePage();
	}

	@Override
	public Iterator<PaginationItem> iterator() {
		return getItems().iterator();
	}

	public Iterator<PaginationItem> getIterator() {
		return iterator();
	}

	/**
	 * Creates a list of pagination items and returns it.
	 * 
	 * @return list of pagination items
	 */
	public List<PaginationItem> getItems() {
		if (items != null) {
			return items;
		}
		items = new LinkedList<PaginationItem>();
		int currentPageNr = pagination.getCurrentPageNr();
		int start = max(1, currentPageNr - nrItemsAroundCurrentPage);
		int stop = min(currentPageNr + nrItemsAroundCurrentPage, pagination.getNrPages());
		for (int nr = start; nr <= stop; ++nr) {
			addItemToBack(nr);
		}
		if (breakFlow) {
			addFlowBreakers(items);
		}
		if (showFirstAndLast) {
			addFirstAndLast(items);
		}
		if (showPreviousAndNext) {
			addPreviousAndNext(items, currentPageNr);
		}
		return items;
	}

	private void addPreviousAndNext(LinkedList<PaginationItem> items, int currentPageNr) {
		if (currentPageNr > 1) {
			PaginationItem item = new PaginationItem(pagination, currentPageNr - 1, false, true, false);
			items.addFirst(item);
		}
		if (currentPageNr < pagination.getNrPages()) {
			PaginationItem item = new PaginationItem(pagination, currentPageNr + 1, false, false, true);
			items.add(item);
		}
	}

	private void addFirstAndLast(LinkedList<PaginationItem> items) {
		int currentPageNr = pagination.getCurrentPageNr();
		if (currentPageNr - nrItemsAroundCurrentPage > 1) {
			addItemToFront(1);
		}
		if (currentPageNr + nrItemsAroundCurrentPage < pagination.getNrPages()) {
			addItemToBack(pagination.getNrPages());
		}
	}

	private void addFlowBreakers(LinkedList<PaginationItem> items) {
		int currentPageNr = pagination.getCurrentPageNr();
		int offset = currentPageNr - nrItemsAroundCurrentPage; 
		if (offset > 1) {
			if (offset == 2) {
				addItemToFront(2);
			} else {
				addFlowBreaker(true);
			}
		}
		offset = currentPageNr + nrItemsAroundCurrentPage;
		if (offset < pagination.getNrPages()) {
			int secondToLastPageNr = pagination.getNrPages() - 1;
			if (offset == secondToLastPageNr) {
				addItemToBack(secondToLastPageNr);
			} else {
				addFlowBreaker(false);
			}
		}
	}

	private void addItemToBack(int nr) {
		addItem(nr, false);
	}

	private void addItemToFront(int nr) {
		addItem(nr, true);
	}

	private void addItem(int nr, boolean front) {
		PaginationItem item = new PaginationItem(pagination, nr, false);
		if (front) {
			items.addFirst(item);
		} else {
			items.add(item);
		}
	}

	private void addFlowBreaker(boolean front) {
		PaginationItem item = new PaginationItem(pagination, -1, true);
		if (front) {
			items.addFirst(item);
		} else {
			items.add(item);
		}
	}

}
